/*
 * Pushlet client using AJAX XMLHttpRequest.
 *
 * DESCRIPTION
 * This file provides self-contained support for using the
 * Pushlet protocol through AJAX-technology. The XMLHttpRequest is used
 * to exchange the Pushlet protocol XML messages (may use JSON in later versions).
 * Currently only HTTP GET is used in asynchronous mode.
 *
 * The Pushlet protocol provides a Publish/Subscribe service for
 * simple messages. The Pushlet server provides session management (join/leave),
 * subscription management (subscribe/unsubscribe), server originated push
 * and publication (publish).
 *
 * For subscriptions server-push is emulated using a single
 * long-lived XMLHttpRequests (Pushlet pull mode) where the server holds the
 * request until events arrive for which a session has subscriptions.
 * This is thus different from polling. In future versions XML streaming
 * may be used since this is currently only supported in Moz-family browsers.
 *
 * Users should supply global callback functions for the events they are interested in.
 * For now see _onEvent() for the specific functions that are called.
 * The most important one is onData(). If onEvent() is available that catches
 * all events. All callback functions have a single
 * argument with a PushletEvent object.
 * A future version should provide a more OO (Observer) approach.
 *
 * EXAMPLES
 * PL.join();
 * PL.listen();
 * PL.subscribe('/temperature');
 * // or shorter
 * PL.joinListen('/temperature');
 * // You provide as callback:
 * onData(pushletEvent);
 * See examples in the Pushlet distribution (e.g. webapps/pushlet/examples/ajax)
 *
 * WHY
 * IMO using XMLHttpRequest has many advantages over the original JS streaming:
 * more stability, no browser busy-bees, better integration with other AJAX frameworks,
 * more debugable, more understandable, ...
 *
 * $Id: ajax-pushlet-client.js,v 1.7 2007/07/27 11:45:08 justb Exp $
 */

/** Namespaced Pushlet functions. */
var PL = {
    NV_P_FORMAT: 'p_format=xml-strict',
    NV_P_MODE: 'p_mode=pull',
    pushletURL: null,
    webRoot: null,
    sessionId: null,
    STATE_ERROR: -2,
    STATE_ABORT: -1,
    STATE_NULL: 1,
    STATE_READY: 2,
    STATE_JOINED: 3,
    STATE_LISTENING: 3,
    state: 1,

    /************** START PUBLIC FUNCTIONS  **************/

    /** Send heartbeat. */
    heartbeat: function () {
        PL._doRequest('hb');
    },

    /** Join. */
    join: function () {
        PL.sessionId = null;

        // Streaming is only supported in Mozilla. E.g. IE does not allow access to responseText on readyState == 3
        PL._doRequest('join', PL.NV_P_FORMAT + '&' + PL.NV_P_MODE);
    },

    /** Join, listen and subscribe. */
    joinListen: function (aSubject) {
        PL._setStatus('join-listen ' + aSubject);
        // PL.join();
        // PL.listen(aSubject);

        PL.sessionId = null;
        // Create event URI for listen
        var query = PL.NV_P_FORMAT + '&' + PL.NV_P_MODE;

        // Optional subject to subscribe to
        if (aSubject) {
            query = query + '&p_subject=' + aSubject;
        }

        PL._doRequest('join-listen', query);

    },

    /** Close pushlet session. */
    leave: function () {
        PL._doRequest('leave');
    },

    /** Listen on event channel. */
    listen: function (aSubject) {

        // Create event URI for listen
        var query = PL.NV_P_MODE;

        // Optional subject to subscribe to
        if (aSubject) {
            query = query + '&p_subject=' + aSubject;
        }

        PL._doRequest('listen', query);
    },

    /** Publish to subject. */
    publish: function (aSubject, theQueryArgs) {

        var query = 'p_subject=' + aSubject;
        if (theQueryArgs) {
            query = query + '&' + theQueryArgs;
        }

        PL._doRequest('publish', query);
    },

    /** Subscribe to (comma separated) subject(s). */
    subscribe: function (aSubject, aLabel) {

        var query = 'p_subject=' + aSubject;
        if (aLabel) {
            query = query + '&p_label=' + aLabel;
        }
        PL._doRequest('subscribe', query);

    },

    /** Unsubscribe from (all) subject(s). */
    unsubscribe: function (aSubscriptionId) {
        var query;

        // If no sid we unsubscribe from all subscriptions
        if (aSubscriptionId) {
            query = 'p_sid=' + aSubscriptionId;
        }
        PL._doRequest('unsubscribe', query);
    },

    setDebug: function (bool) {
        PL.debugOn = bool;
    },


    /************** END PUBLIC FUNCTIONS  **************/

// Cross-browser add event listener to element
    _addEvent: function (elm, evType, callback, useCapture) {
        var obj = PL._getObject(elm);
        if (obj.addEventListener) {
            obj.addEventListener(evType, callback, useCapture);
            return true;
        } else if (obj.attachEvent) {
            var r = obj.attachEvent('on' + evType, callback);
            return r;
        } else {
            obj['on' + evType] = callback;
        }
    },

    _doCallback: function (event, cbFunction) {
        // Do specific callback function if provided by client
        if (cbFunction) {
            // Do specific callback like onData(), onJoinAck() etc.
            cbFunction(event);
        } else if (window.onEvent) {
            // general callback onEvent() provided to catch all events
            onEvent(event);
        }
    },

// Do XML HTTP request
    _doRequest: function (anEvent, aQuery) {
        // Check if we are not in any error state
        if (PL.state < 0) {
            PL._setStatus('died (' + PL.state + ')');
            return;
        }

        // We may have (async) requests outstanding and thus
        // may have to wait for them to complete and change state.
        var waitForState = false;
        if (anEvent == 'join' || anEvent == 'join-listen') {
            // We can only join after initialization
            waitForState = (PL.state < PL.STATE_READY);
        } else if (anEvent == 'leave') {
            PL.state = PL.STATE_READY;
        } else if (anEvent == 'refresh') {
            // We must be in the listening state
            if (PL.state != PL.STATE_LISTENING) {
                return;
            }
        } else if (anEvent == 'listen') {
            // We must have joined before we can listen
            waitForState = (PL.state < PL.STATE_JOINED);
        } else if (anEvent == 'subscribe' || anEvent == 'unsubscribe') {
            // We must be listeing for subscription mgmnt
            waitForState = (PL.state < PL.STATE_LISTENING);
        } else {
            // All other requests require that we have at least joined
            waitForState = (PL.state < PL.STATE_JOINED);
        }

        // May have to wait for right state to issue request
        if (waitForState == true) {
            PL._setStatus(anEvent + ' , waiting... state=' + PL.state);
            setTimeout(function () {
                PL._doRequest(anEvent, aQuery);
            }, 100);
            return;
        }

        // ASSERTION: PL.state is OK for this request

        // Construct base URL for GET
        var url = PL.pushletURL + '?p_event=' + anEvent;

        // Optionally attach query string
        if (aQuery) {
            url = url + '&' + aQuery;
        }

        // Optionally attach session id
        if (PL.sessionId != null) {
            url = url + '&p_id=' + PL.sessionId;
            if (anEvent == 'p_leave') {
                PL.sessionId = null;
            }
        }
        PL.debug('_doRequest', url);
        PL._getXML(url, PL._onResponse);

        // uncomment to use synchronous XmlHttpRequest
        //var rsp = PL._getXML(url);
        //PL._onResponse(rsp);  */
    },

// Get object reference
    _getObject: function (obj) {
        if (typeof obj == "string") {
            return document.getElementById(obj);
        } else {
            // pass through object reference
            return obj;
        }
    },

    _getWebRoot: function () {
        /** Return directory of this relative to document URL. */
        if (PL.webRoot != null) {
            return PL.webRoot;
        }
        //derive the baseDir value by looking for the script tag that loaded this file
        var head = document.getElementsByTagName('head')[0];
        var nodes = head.childNodes;
        for (var i = 0; i < nodes.length; ++i) {
            var src = nodes.item(i).src;
            if (src) {
                var index = src.indexOf("ajax-pushlet-client.js");
                if (index >= 0) {
                    index = src.indexOf("lib");
                    PL.webRoot = src.substring(0, index);
                    break;
                }
            }
        }
        return PL.webRoot;
    },

// Get XML doc from server
// On response  optional callback fun is called with optional user data.
    _getXML: function (url, callback) {

        // Obtain XMLHttpRequest object
        var xmlhttp = new XMLHttpRequest();
        if (!xmlhttp || xmlhttp == null) {
            alert('No browser XMLHttpRequest (AJAX) support');
            return;
        }

        // Setup optional async response handling via callback
        var cb = callback;
        var async = false;

        if (cb) {
            // Async mode
            async = true;
            xmlhttp.onreadystatechange = function () {
                if (xmlhttp.readyState == 4) {
                    if (xmlhttp.status == 200) {
                        // Processing statements go here...
                        cb(xmlhttp.responseXML);

                        // Avoid memory leaks in IE
                        // 12.may.2007 thanks to Julio Santa Cruz
                        xmlhttp = null;
                    } else {
                        var event = new PushletEvent();
                        event.put('p_event', 'error')
                        event.put('p_reason', '[pushlet] problem retrieving XML data:\n' + xmlhttp.statusText);
                        PL._onEvent(event);
                    }
                }
            };
        }
        // Open URL
        xmlhttp.open('GET', url, async);

        // Send XML to KW server
        xmlhttp.send(null);

        if (!cb) {
            if (xmlhttp.status != 200) {
                var event = new PushletEvent();
                event.put('p_event', 'error')
                event.put('p_reason', '[pushlet] problem retrieving XML data:\n' + xmlhttp.statusText);
                PL._onEvent(event)
                return null;
            }
            // Sync mode (no callback)
            // alert(xmlhttp.responseText);

            return xmlhttp.responseXML;
        }
    },


    _init: function () {
        PL._showStatus();
        PL._setStatus('initializing...');
        /*
         Setup Cross-Browser XMLHttpRequest v1.2
         Emulate Gecko 'XMLHttpRequest()' functionality in IE and Opera. Opera requires
         the Sun Java Runtime Environment <http://www.java.com/>.

         by Andrew Gregory
         http://www.scss.com.au/family/andrew/webdesign/xmlhttprequest/

         This work is licensed under the Creative Commons Attribution License. To view a
         copy of this license, visit http://creativecommons.org/licenses/by-sa/2.5/ or
         send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, California
         94305, USA.

         */
        // IE support
        if (window.ActiveXObject && !window.XMLHttpRequest) {
            window.XMLHttpRequest = function () {
                var msxmls = new Array(
                    'Msxml2.XMLHTTP.5.0',
                    'Msxml2.XMLHTTP.4.0',
                    'Msxml2.XMLHTTP.3.0',
                    'Msxml2.XMLHTTP',
                    'Microsoft.XMLHTTP');
                for (var i = 0; i < msxmls.length; i++) {
                    try {
                        return new ActiveXObject(msxmls[i]);
                    } catch (e) {
                    }
                }
                return null;
            };
        }

        // ActiveXObject emulation
        if (!window.ActiveXObject && window.XMLHttpRequest) {
            window.ActiveXObject = function (type) {
                switch (type.toLowerCase()) {
                    case 'microsoft.xmlhttp':
                    case 'msxml2.xmlhttp':
                    case 'msxml2.xmlhttp.3.0':
                    case 'msxml2.xmlhttp.4.0':
                    case 'msxml2.xmlhttp.5.0':
                        return new XMLHttpRequest();
                }
                return null;
            };
        }

        // PL.pushletURL = 'pushlet.srv';
        // PL.pushletURL = PL._getWebRoot() + 'pushlet.srv';
        PL.pushletURL = '/pushlet/pushlet.srv';
        PL._setStatus('initialized');
        PL.state = PL.STATE_READY;
    },

    /** Handle incoming events from server. */
    _onEvent: function (event) {
        // Create a PushletEvent object from the arguments passed in
        // push.arguments is event data coming from the Server

        PL.debug('_onEvent()', event.toString());

        // Do action based on event type
        var eventType = event.getEvent();

        if (eventType == 'data') {
            PL._setStatus('data');
            PL._doCallback(event, window.onData);
        } else if (eventType == 'refresh') {
            if (PL.state < PL.STATE_LISTENING) {
                PL._setStatus('not refreshing state=' + PL.STATE_LISTENING);
            }
            var timeout = event.get('p_wait');
            setTimeout(function () {
                PL._doRequest('refresh');
            }, timeout);
            return;
        } else if (eventType == 'error') {
            PL.state = PL.STATE_ERROR;
            PL._setStatus('server error: ' + event.get('p_reason'));
            PL._doCallback(event, window.onError);
        } else if (eventType == 'join-ack') {
            PL.state = PL.STATE_JOINED;
            PL.sessionId = event.get('p_id');
            PL._setStatus('connected');
            PL._doCallback(event, window.onJoinAck);
        } else if (eventType == 'join-listen-ack') {
            PL.state = PL.STATE_LISTENING;
            PL.sessionId = event.get('p_id');
            PL._setStatus('join-listen-ack');
            PL._doCallback(event, window.onJoinListenAck);
        } else if (eventType == 'listen-ack') {
            PL.state = PL.STATE_LISTENING;
            PL._setStatus('listening');
            PL._doCallback(event, window.onListenAck);
        } else if (eventType == 'hb') {
            PL._setStatus('heartbeat');
            PL._doCallback(event, window.onHeartbeat);
        } else if (eventType == 'hb-ack') {
            PL._doCallback(event, window.onHeartbeatAck);
        } else if (eventType == 'leave-ack') {
            PL._setStatus('disconnected');
            PL._doCallback(event, window.onLeaveAck);
        } else if (eventType == 'refresh-ack') {
            PL._doCallback(event, window.onRefreshAck);
        } else if (eventType == 'subscribe-ack') {
            PL._setStatus('subscribed to ' + event.get('p_subject'));
            PL._doCallback(event, window.onSubscribeAck);
        } else if (eventType == 'unsubscribe-ack') {
            PL._setStatus('unsubscribed');
            PL._doCallback(event, window.onUnsubscribeAck);
        } else if (eventType == 'abort') {
            PL.state = PL.STATE_ERROR;
            PL._setStatus('abort');
            PL._doCallback(event, window.onAbort);
        } else if (eventType.match(/nack$/)) {
            PL._setStatus('error response: ' + event.get('p_reason'));
            PL._doCallback(event, window.onNack);
        }
    },

    /**  Handle XMLHttpRequest response XML. */
    _onResponse: function (xml) {
        PL.debug('_onResponse', xml);
        var events = PL._rsp2Events(xml);
        if (events == null) {
            PL._setStatus('null events')
            return;
        }

        delete xml;

        PL.debug('_onResponse eventCnt=', events.length);
        // Go through all <event/> elements
        for (i = 0; i < events.length; i++) {
            PL._onEvent(events[i]);
        }
    },

    /** Convert XML response to PushletEvent objects. */
    _rsp2Events: function (xml) {
        // check empty response or xml document
        if (!xml || !xml.documentElement) {
            return null;
        }

        // Convert xml doc to array of PushletEvent objects
        var eventElements = xml.documentElement.getElementsByTagName('event');
        var events = new Array(eventElements.length);
        for (i = 0; i < eventElements.length; i++) {
            events[i] = new PushletEvent(eventElements[i]);
        }

        return events;

    },

    statusMsg: 'null',
    statusChanged: false,
    statusChar: '|',


    _showStatus: function () {
        // To show progress
        if (PL.statusChanged == true) {
            if (PL.statusChar == '|') PL.statusChar = '/';
            else if (PL.statusChar == '/') PL.statusChar = '--';
            else if (PL.statusChar == '--') PL.statusChar = '\\';
            else PL.statusChar = '|';
            PL.statusChanged = false;
        }
        window.defaultStatus = PL.statusMsg;
        window.status = PL.statusMsg + '  ' + PL.statusChar;
        timeout = window.setTimeout('PL._showStatus()', 400);
    },

    _setStatus: function (status) {
        PL.statusMsg = "pushlet - " + status;
        PL.statusChanged = true;
    },


    /*************** Debug utility *******************************/
    timestamp: 0,
    debugWindow: null,
    messages: new Array(),
    messagesIndex: 0,
    debugOn: false,

    /** Send debug messages to a (D)HTML window. */
    debug: function (label, value) {
        if (PL.debugOn == false) {
            return;
        }
        var funcName = "none";

        // Fetch JS function name if any
        if (PL.debug.caller) {
            funcName = PL.debug.caller.toString()
            funcName = funcName.substring(9, funcName.indexOf(")") + 1)
        }

        // Create message
        var msg = "-" + funcName + ": " + label + "=" + value

        // Add optional timestamp
        var now = new Date()
        var elapsed = now - PL.timestamp
        if (elapsed < 10000) {
            msg += " (" + elapsed + " msec)"
        }

        PL.timestamp = now;

        // Show.

        if ((PL.debugWindow == null) || PL.debugWindow.closed) {
            PL.debugWindow = window.open("", "p_debugWin", "toolbar=no,scrollbars=yes,resizable=yes,width=600,height=400");
        }

        // Add message to current list
        PL.messages[PL.messagesIndex++] = msg

        // Write doc header
        PL.debugWindow.document.writeln('<html><head><title>Pushlet Debug Window</title></head><body bgcolor=#DDDDDD>');

        // Write the messages
        for (var i = 0; i < PL.messagesIndex; i++) {
            PL.debugWindow.document.writeln('<pre>' + i + ': ' + PL.messages[i] + '</pre>');
        }

        // Write doc footer and close
        PL.debugWindow.document.writeln('</body></html>');
        PL.debugWindow.document.close();
        PL.debugWindow.focus();

    }


}


/* Represents nl.justobjects.pushlet.Event in JS. */
function PushletEvent(xml) {
    // Member variable setup; the assoc array stores the N/V pairs
    this.arr = new Array();

    this.getSubject = function () {
        return this.get('p_subject');
    }

    this.getEvent = function () {
        return this.get('p_event');
    }

    this.put = function (name, value) {
        return this.arr[name] = value;
    }

    this.get = function (name) {
        return this.arr[name];
    }

    this.toString = function () {
        var res = '';
        for (var i in this.arr) {
            res = res + i + '=' + this.arr[i] + '\n';
        }
        return res;
    }

    this.toTable = function () {
        var res = '<table border="1" cellpadding="3">';
        var styleDiv = '<div style="color:black; font-family:monospace; font-size:10pt; white-space:pre;">'

        for (var i in this.arr) {
            res = res + '<tr><td bgColor=white>' + styleDiv + i + '</div></td><td bgColor=white>' + styleDiv + this.arr[i] + '</div></td></tr>';
        }
        res += '</table>'
        return res;
    }

    // Optional XML element <event name="value" ... />
    if (xml) {
        // Put the attributes in Map
        for (var i = 0; i < xml.attributes.length; i++) {
            this.put(xml.attributes[i].name, xml.attributes[i].value);
        }
    }
}

/**********************************************************************
 START - OLD application functions (LEFT HERE FOR FORWARD COMPAT)
 ***********************************************************************/
// Debug util
function p_debug(aBool, aLabel, aMsg) {
    if (aBool == false) {
        return;
    }

    PL.setDebug(true);
    PL.debug(aLabel, aMsg);
    PL.setDebug(false);
}

// Embed pushlet frame in page (OBSOLETE)
function p_embed(thePushletWebRoot) {
    alert('Pushlet: p_embed() is no longer required for AJAX client')
}

// Join the pushlet server
function p_join() {
    PL.join();
}

// Create data event channel with the server
function p_listen(aSubject, aMode) {
    // Note: mode is fixed to 'pull'
    PL.listen(aSubject);
}

// Shorthand: Join the pushlet server and start listening immediately
function p_join_listen(aSubject) {
    PL.joinListen(aSubject);
}

// Leave the pushlet server
function p_leave() {
    PL.leave();
}

// Send heartbeat event; callback is onHeartbeatAck()
function p_heartbeat() {
    PL.heartbeat();
}

// Publish to a subject
function p_publish(aSubject, nvPairs) {
    var args = p_publish.arguments;

    // Put the arguments' name/value pairs in the URI
    var query = '';
    var amp = '';
    for (var i = 1; i < args.length; i++) {
        if (i > 1) {
            amp = '&';
        }
        query = query + amp + args[i] + '=' + args[++i];
    }
    PL.publish(aSubject, query);
}

// Subscribe to a subject with optional label
function p_subscribe(aSubject, aLabel) {
    PL.subscribe(aSubject, aLabel);
}

// Unsubscribe from a subject
function p_unsubscribe(aSid) {
    PL.unsubscribe(aSid);
}

/**********************************************************************
 END - Public application functions (LEFT HERE FOR FORWARD COMPAT)
 ***********************************************************************/

// Initialize when page completely loaded
PL._addEvent(window, 'load', PL._init, false);


